HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import { ScrollX } from '@/components/models/scroll-x';3import Link from 'next/link';4import { notFound, permanentRedirect } from 'next/navigation';5import { CompareTrayBar } from '@/components/compare/compare-tray-bar';6import { CompareButton } from '@/components/compare/compare-button';7import { ViewBeacon } from '@/components/layout/view-beacon';8import { OpennessChip } from '@/components/models/badges';9import { FamilyBenchmarkProgress, FamilyReleaseLanes } from '@/components/models/family-blocks';10import { MiniGraph } from '@/components/models/mini-graph';11import { Chip, EntityBadge, StatusBadge } from '@/components/ui/badges';12import { DataTable, Td, Th } from '@/components/ui/data-table';13import { EntityLink, QualityMark } from '@/components/ui/entity';14import { Hint } from '@/components/ui/hint';15import { Container, Note } from '@/components/ui/section';16import { EmptyState } from '@/components/ui/unavailable';17import { ApiError, apiD1, safe } from '@/lib/api';18import { fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';19import { routes, SITE_NAME, SITE_URL } from '@/lib/site';20import type { FamilyDetail } from '@/lib/types';2122type Params = { params: Promise<{ slug: string }> };23export const revalidate = 600;2425async function load(slug: string): Promise<FamilyDetail> {26 try {27 return await apiD1.family(slug, 300);28 } catch (e) {29 if (e instanceof ApiError && e.notFound) notFound();30 throw e;31 }32}3334function describe(f: FamilyDetail): string {35 const pmin = num(f.param_range?.min);36 const pmax = num(f.param_range?.max);37 const bits = [`${fmtInt(f.model_count)} canonical models`, f.first_release ? `released ${fmtDate(f.first_release)} → ${f.last_release ? fmtDate(f.last_release) : 'today'}` : null, pmin !== null ? `${fmtParams(pmin)}${pmax !== null && pmax !== pmin ? ` – ${fmtParams(pmax)}` : ''} parameters` : null, f.licenses?.length ? `licences ${f.licenses.map((l) => l.key).slice(0, 3).join(', ')}` : null, num(f.artifacts_count) ? `${fmtInt(f.artifacts_count)} artifacts` : null].filter(Boolean);38 return `${f.name}${f.organization ? ` by ${f.organization.name}` : ''} — ${bits.join('; ')}. Release timeline, members, lineage, licence mix and benchmark progress on ${SITE_NAME}.`.slice(0, 300);39}4041export async function generateMetadata({ params }: Params): Promise<Metadata> {42 const { slug } = await params;43 const f = await safe(apiD1.family(slug, 5));44 if (!f || f.slug !== slug) return { title: 'Family', robots: { index: false } };45 const title = `${f.name} family — Releases, Members, Lineage & Benchmarks`;46 const description = describe(f);47 const canonical = routes.family(f.slug);48 return { title, description, alternates: { canonical }, openGraph: { title: `${title} | ${SITE_NAME}`, description, url: `${SITE_URL}${canonical}`, type: 'article', siteName: SITE_NAME }, twitter: { card: 'summary_large_image', title, description } };49}5051export default async function FamilyPage({ params }: Params) {52 const { slug } = await params;53 const f = await load(slug);54 if (f.slug && f.slug !== slug) permanentRedirect(routes.family(f.slug));55 const bench = await safe(apiD1.benchmarks());56 const benchNames: Record<string, string> = {};57 for (const b of bench?.items ?? []) benchNames[b.slug] = b.name;58 const canonical = routes.family(f.slug);59 const members = [...f.members].sort((a, b) => String(b.key_facts?.release_date ?? '').localeCompare(String(a.key_facts?.release_date ?? '')) || a.model.name.localeCompare(b.model.name));60 const pmin = num(f.param_range?.min);61 const pmax = num(f.param_range?.max);62 const nodes = f.members.map((m) => ({ id: m.model.id, label: m.model.name, href: routes.entity(m.model), sub: num(m.model.attributes?.parameter_count) !== null ? fmtParams(m.model.attributes.parameter_count) : undefined }));63 const licTotal = f.licenses.reduce((n, l) => n + (num(l.models) ?? 0), 0);64 const ranks = Object.entries(f.benchmark_best ?? {}).sort((a, b) => a[1].rank - b[1].rank);65 const root = typeof f.summary?.attributes?.family_root === 'string' ? (f.summary.attributes.family_root as string) : null;66 const ld = { '@context': 'https://schema.org', '@type': 'CreativeWorkSeries', name: f.name, url: `${SITE_URL}${canonical}`, description: describe(f), creator: f.organization ? { '@type': 'Organization', name: f.organization.name } : undefined, hasPart: members.slice(0, 20).map((m) => ({ '@type': 'SoftwareApplication', name: m.model.name, url: `${SITE_URL}${routes.entity(m.model)}` })) };6768 return (69 <Container wide>70 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />71 <ViewBeacon path={canonical} />72 <nav aria-label="Breadcrumb" className="pt-5 text-xs text-ink-3">73 <ol className="flex flex-wrap items-center gap-1.5">74 <li>75 <Link href="/" className="hover:text-ink">76 AI Atlas77 </Link>78 </li>79 <li aria-hidden>/</li>80 <li>81 <Link href={routes.families()} className="hover:text-ink">82 Families83 </Link>84 </li>85 {f.organization && (86 <>87 <li aria-hidden>/</li>88 <li>89 <Link href={routes.entity({ entity_type: 'company', slug: f.organization.slug })} className="hover:text-ink">90 {f.organization.name}91 </Link>92 </li>93 </>94 )}95 <li aria-hidden>/</li>96 <li className="text-ink-2">{f.name}</li>97 </ol>98 </nav>99 <header className="pb-6 pt-4 md:pt-5" data-family-header>100 <div className="flex flex-wrap items-center gap-2">101 <EntityBadge type="model_family" />102 {!f.canonical && (103 <span className="inline-flex items-center gap-0.5 rounded-[3px] bg-warning-soft px-1.5 text-[11px] font-medium text-warning">104 label-based family105 <Hint text={f.note ?? 'Grouped by the family label stated by sources; not yet a canonical model_family entity. Counts may change when canonicalised.'} />106 </span>107 )}108 {root && root !== f.name && <span className="text-xs text-ink-3">root · {root}</span>}109 </div>110 <div className="mt-3 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">111 <div className="min-w-0">112 <h1 className="display text-[30px] md:text-[44px]">{f.name}</h1>113 <p className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[15px] text-ink-2">114 {f.organization && (115 <Link href={routes.entity({ entity_type: 'company', slug: f.organization.slug })} className="font-medium text-ink hover:text-accent">116 {f.organization.name}117 </Link>118 )}119 <Link href={`/models?family=${encodeURIComponent(f.slug)}`} className="link">120 All {fmtInt(f.model_count)} models in the terminal →121 </Link>122 </p>123 {f.summary?.description && <p className="mt-3 max-w-3xl text-[15px] leading-relaxed text-ink-2">{f.summary.description}</p>}124 </div>125 {f.summary && (126 <div className="shrink-0 text-xs text-ink-3 lg:text-right">127 <QualityMark q={f.summary.quality?.score} label />128 <p className="mt-1">first seen {fmtDate(f.summary.first_seen_at)}</p>129 <p className="mono mt-0.5 text-[11px]">{f.id}</p>130 </div>131 )}132 </div>133 <dl className="tnum mt-4 grid grid-cols-2 gap-x-6 gap-y-3 border-y border-rule py-3 sm:grid-cols-3 lg:grid-cols-6" data-family-strip>134 {[135 ['Models', fmtInt(f.model_count)],136 ['First release', f.first_release ? fmtDate(f.first_release) : '—'],137 ['Latest release', f.last_release ? fmtDate(f.last_release) : '—'],138 ['Parameters', pmin === null ? '—' : pmin === pmax ? fmtParams(pmin) : `${fmtParams(pmin)} – ${fmtParams(pmax)}`],139 ['Artifacts', fmtInt(f.artifacts_count)],140 ['Providers', fmtInt(f.providers?.length ?? 0)],141 ].map(([k, v]) => (142 <div key={k} className="min-w-0">143 <dt className="eyebrow">{k}</dt>144 <dd className="mt-0.5 truncate text-[15px] font-medium text-ink">{v}</dd>145 </div>146 ))}147 </dl>148 </header>149150 <div className="space-y-10 pb-16">151 <section id="releases">152 <p className="eyebrow mb-2">Releases</p>153 <FamilyReleaseLanes members={f.members} />154 </section>155156 <section id="members">157 <p className="eyebrow mb-2">158 Members <span className="tnum text-ink-3">{fmtInt(members.length)}</span>159 </p>160 {members.length === 0 ? (161 <EmptyState title="No member recorded" />162 ) : (163 <ScrollX><DataTable caption="Family members" compact>164 <thead>165 <tr>166 <Th>Model</Th>167 <Th num>Params</Th>168 <Th num>Context</Th>169 <Th>Openness</Th>170 <Th>Licence</Th>171 <Th>Released</Th>172 <Th>Status</Th>173 <Th>Best rank</Th>174 <Th className="text-right">175 <span className="sr-only">Compare</span>176 </Th>177 </tr>178 </thead>179 <tbody>180 {members.map((m) => {181 const a = m.model.attributes ?? {};182 const kf = m.key_facts ?? {};183 const best = Object.entries(m.benchmark_ranks ?? {}).sort((x, y) => x[1] - y[1])[0];184 const status = String(kf.status ?? m.model.status ?? '');185 return (186 <tr key={m.model.id}>187 <Td primary>188 <EntityLink e={m.model} />189 {m.model.organization && m.model.organization.slug !== f.organization?.slug && <span className="block text-[11px] text-ink-3">{m.model.organization.name}</span>}190 </Td>191 <Td num label="Params" className="tnum">192 {num(a.parameter_count) === null ? <span className="text-ink-3">—</span> : fmtParams(a.parameter_count)}193 {num(a.active_parameter_count) !== null && num(a.active_parameter_count) !== num(a.parameter_count) && <span className="block text-[11px] text-ink-3">{fmtParams(a.active_parameter_count)} active</span>}194 </Td>195 <Td num label="Context" className="tnum">196 {num(kf.context_length ?? a.context_length) === null ? <span className="text-ink-3">—</span> : fmtTokens(kf.context_length ?? a.context_length)}197 </Td>198 <Td label="Openness">{typeof (kf.openness ?? a.openness) === 'string' ? <OpennessChip openness={String(kf.openness ?? a.openness)} /> : <span className="text-ink-3">—</span>}</Td>199 <Td label="Licence" className="max-w-[10rem] truncate text-xs text-ink-2">200 {typeof (a.license_key ?? a.license) === 'string' ? <Link href={`/licenses/${encodeURIComponent(String(a.license_key ?? a.license))}`} className="hover:text-accent">{String(a.license_key ?? a.license)}</Link> : <span className="text-ink-3">—</span>}201 </Td>202 <Td label="Released" className="tnum whitespace-nowrap text-ink-2">203 {typeof kf.release_date === 'string' ? fmtDate(kf.release_date) : <span className="text-ink-3">—</span>}204 </Td>205 <Td label="Status">206 <StatusBadge status={status} />207 </Td>208 <Td label="Best rank" className="text-xs text-ink-2">209 {best ? (210 <Link href={routes.benchmark(best[0])} className="whitespace-nowrap hover:text-accent" title={benchNames[best[0]] ?? best[0]}>211 <span className="tnum font-medium text-ink">#{fmtInt(best[1])}</span> {(benchNames[best[0]] ?? best[0]).replace('Artificial Analysis ', 'AA ').slice(0, 22)}212 </Link>213 ) : (214 <span className="text-ink-3">—</span>215 )}216 </Td>217 <Td className="text-right">218 <CompareButton e={m.model} size="sm" label="" />219 </Td>220 </tr>221 );222 })}223 </tbody>224 </DataTable></ScrollX>225 )}226 </section>227228 <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_22rem]">229 <section id="lineage" className="min-w-0">230 <div className="mb-2 flex items-baseline justify-between gap-2">231 <p className="eyebrow">232 Lineage among members <span className="tnum text-ink-3">{fmtInt(f.lineage.length)} relations</span>233 </p>234 {f.summary && (235 <Link href={`${routes.graph(f.summary.slug)}?mode=lineage`} className="link text-xs">236 Open in Graph →237 </Link>238 )}239 </div>240 <MiniGraph nodes={nodes} edges={f.lineage} title={`Lineage of the ${f.name} family`} />241 </section>242 <aside className="min-w-0 space-y-8">243 <section id="licences">244 <p className="eyebrow mb-2">Licence mix</p>245 {f.licenses.length ? (246 <ul className="space-y-1.5 text-sm">247 {f.licenses.map((l) => (248 <li key={l.key} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3">249 <Link href={`/licenses/${encodeURIComponent(l.key)}`} className="truncate text-ink hover:text-accent" title={l.label}>250 {l.label}251 </Link>252 <span className="tnum text-xs text-ink-2">{fmtInt(l.models)}</span>253 <span className="col-span-2 h-1 rounded-sm bg-surface-2">254 <span className="block h-full rounded-sm bg-accent" style={{ width: `${licTotal ? (100 * (num(l.models) ?? 0)) / licTotal : 0}%` }} />255 </span>256 </li>257 ))}258 </ul>259 ) : (260 <p className="text-sm text-ink-3">No licence recorded on the members.</p>261 )}262 </section>263 <section id="providers">264 <p className="eyebrow mb-2">265 Providers <span className="tnum text-ink-3">{fmtInt(f.providers?.length ?? 0)}</span>266 </p>267 {f.providers?.length ? (268 <p className="flex flex-wrap gap-x-3 gap-y-1 text-sm">269 {f.providers.map((p) => (270 <EntityLink key={p.id} e={p} />271 ))}272 </p>273 ) : (274 <p className="text-sm text-ink-3">No provider deployment recorded.</p>275 )}276 </section>277 <section id="modalities">278 <p className="eyebrow mb-2">Modalities</p>279 <p className="flex flex-wrap gap-1">{f.modalities?.length ? f.modalities.map((m) => <Chip key={m}>{m}</Chip>) : <span className="text-sm text-ink-3">—</span>}</p>280 </section>281 {ranks.length > 0 && (282 <section id="best-ranks">283 <p className="eyebrow mb-2">Best benchmark ranks</p>284 <ul className="divide-y divide-rule border-y border-rule text-sm">285 {ranks.slice(0, 8).map(([slug, r]) => (286 <li key={slug} className="grid grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-3 py-1.5">287 <span className="tnum font-medium">#{fmtInt(r.rank)}</span>288 <span className="min-w-0 truncate">289 <Link href={routes.benchmark(slug)} className="text-ink hover:text-accent">290 {benchNames[slug] ?? slug}291 </Link>292 <span className="block truncate text-[11px] text-ink-3">{r.model}</span>293 </span>294 </li>295 ))}296 </ul>297 </section>298 )}299 </aside>300 </div>301302 <section id="progress">303 <p className="eyebrow mb-2">Benchmark progress</p>304 <FamilyBenchmarkProgress members={f.members} benchNames={benchNames} />305 </section>306 <Note>307 Aggregates are computed from the members' sourced claims; members without a release date or parameter count are left out of the range and timeline rather than guessed. <Link href="/methodology" className="link">Methodology →</Link>308 </Note>309 </div>310 <CompareTrayBar />311 </Container>312 );313}314